package com.ruoyi.plus.controller;

import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.plus.controller.po.MultipartFileChunk;
import com.ruoyi.plus.utils.minio.MinIoUtils;
import io.minio.ComposeSource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.*;

/**
 * @author LIQS
 */
@Slf4j
@RestController
@RequestMapping("/plus/bigFileUpload")
@DependsOn("ruoYiConfig")
public class BigFileUploadController {

    public static final String RESOURCE_ROOT = "/resource";
    private static final String REDIS_KEY_BIG_FILE = "big_file:";
    private static final RedisTemplate redisTemplate = SpringUtils.getBean("redisTemplate");

    private String getLocalChunkDir(MultipartFileChunk chunk) {
        Path path = Paths.get(RuoYiConfig.getProfile() + RESOURCE_ROOT + "/bigFile/chunk/" + chunk.getIdentifier());
        //判断uploadFolder/identifier 路径是否存在，不存在则创建
        if (!Files.isWritable(path)) {
            try {
                Files.createDirectories(path);
            } catch (IOException e) {
                log.error("创建文件失败,错误信息为{}", e.getMessage(), e);
            }
        }
        return path.toString().replace("\\", "/").replace("//", "/");
    }

    private String getMinioChunkDir(MultipartFileChunk chunk) {
        Path path = Paths.get(RESOURCE_ROOT + "/bigFile/chunk/" + chunk.getIdentifier());
        return path.toString().replace("\\", "/").replace("//", "/");
    }

    private String getLocalMergeDir(MultipartFileChunk chunk) {
        Path path = Paths.get(RuoYiConfig.getProfile() + RESOURCE_ROOT + "/" + chunk.getDirPath());
        //判断uploadFolder/identifier 路径是否存在，不存在则创建
        if (!Files.isWritable(path)) {
            try {
                Files.createDirectories(path);
            } catch (IOException e) {
                log.error("创建文件失败,错误信息为{}", e.getMessage(), e);
            }
        }
        return path.toString().replace("\\", "/").replace("//", "/");
    }

    private String getMinioMergeDir(MultipartFileChunk chunk) {
        Path path = Paths.get(RESOURCE_ROOT + "/" + chunk.getDirPath());
        return path.toString().replace("\\", "/").replace("//", "/");
    }


    @GetMapping("/chunk")
    public Map<String, Object> checkChunks(MultipartFileChunk chunk) {
        return checkChunkExits(chunk);
    }

    @PostMapping("/chunk")
    public Map<String, Object> saveChunk(MultipartFileChunk chunk) {
        MultipartFile file = chunk.getFile();
        Integer chunkNumber = chunk.getChunkNumber();
        String identifier = chunk.getIdentifier();
        byte[] bytes;
        try {
            bytes = file.getBytes();
            if (MinIoUtils.config.isEnabled()) {
                String path = getMinioChunkDir(chunk) + "/" + chunk.getChunkNumber();
                MinIoUtils.uploadStream(new ByteArrayInputStream(bytes), path, file.getContentType());
            } else {
                String path = getLocalChunkDir(chunk) + "/" + chunk.getChunkNumber();
                Files.write(Paths.get(path), bytes);
            }
        } catch (IOException e) {
            log.error("上传文件失败,文件块编号为{},错误信息为{}", chunk.getChunkNumber(), e.getMessage(), e);
        }
        Integer chunks = saveChunk(chunkNumber, identifier);
        Map<String, Object> result = new HashMap<>(16);
        if (chunks.equals(chunk.getTotalChunks())) {
            result.put("message", "上传成功！");
            result.put("code", 205);
        }
        return result;
    }

    @PostMapping("/merge")
    @ResponseBody
    public AjaxResult mergeChunks(MultipartFileChunk chunk) {
        String fileUrl = null;
        try {
            if (MinIoUtils.config.isEnabled()) {
                fileUrl = mergeMinioFile(chunk, getMinioChunkDir(chunk), getMinioMergeDir(chunk), chunk.getFilename());
            } else {
                fileUrl = mergeLocalFile(chunk, getLocalChunkDir(chunk), getLocalMergeDir(chunk), chunk.getFilename());
            }
        } catch (Exception e) {
            return AjaxResult.error("上传失败", e.getMessage());
        }
        return AjaxResult.success("上传成功", fileUrl);
    }

    private String mergeMinioFile(MultipartFileChunk chunk, String chunkDir, String mergeDir, String fileName) throws Exception {
        try {
            //Iterable<Result<Item>> results = MinIoUtils.minioClient.listObjects(ListObjectsArgs.builder().prefix(fileName).recursive(false).build());
            List<ComposeSource> composeSourceList = new ArrayList<>();
            Set<Integer> chunkNumber = (Set<Integer>) redisTemplate.opsForHash().get(REDIS_KEY_BIG_FILE + chunk.getIdentifier(), "chunkNumberList");
            for (Integer i : chunkNumber) {
                composeSourceList.add(ComposeSource.builder().bucket(MinIoUtils.config.getBucketName()).object(chunkDir + "/" + i).build());
            }
            return MinIoUtils.composeObject(composeSourceList, mergeDir + "/" + fileName);
        } catch (Exception e) {
            log.error("合并文件失败,文件名为{},错误信息为{}", fileName, e.getMessage(), e);
            String redisKey = REDIS_KEY_BIG_FILE + chunk.getIdentifier();
            if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) {
                redisTemplate.delete(redisKey);
            }
            throw new RuntimeException(e);
        }
    }

    /**
     * 合并文件
     */
    private String mergeLocalFile(MultipartFileChunk chunk, String chunkDir, String mergeDir, String fileName) throws Exception {
        try {
            Path targetFile = Paths.get(mergeDir + "/" + fileName);
            if (!Files.exists(targetFile.getParent())) {
                Files.createDirectories(targetFile.getParent());
            }
            //先删除再创建，防止文件多次合并变大问题。
            if (Files.exists(targetFile)) {
                Files.delete(targetFile);
            }
            Files.createFile(targetFile);
            Set<Integer> chunkNumber = (Set<Integer>) redisTemplate.opsForHash().get(REDIS_KEY_BIG_FILE + chunk.getIdentifier(), "chunkNumberList");
            for (Integer i : chunkNumber) {
                String path = chunkDir + "/" + i;

                //以追加的形式写入文件
                Files.write(targetFile, Files.readAllBytes(Paths.get(path)), StandardOpenOption.APPEND);
            }
            //获取url
            String fileUrl = RuoYiConfig.getUrlPrefix() + "/api/profile" + RESOURCE_ROOT + "/" + chunk.getDirPath() + "/" + fileName;
            HashOperations<Object, Object, Object> hash = redisTemplate.opsForHash();
            hash.put(REDIS_KEY_BIG_FILE + chunk.getIdentifier(), "fileUrl", fileUrl);

            return fileUrl;
        } catch (Exception e) {
            log.error("合并文件失败,文件名为{},错误信息为{}", fileName, e.getMessage(), e);
            String redisKey = REDIS_KEY_BIG_FILE + chunk.getIdentifier();
            if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) {
                redisTemplate.delete(redisKey);
            }
            throw new RuntimeException(e);
        }
    }

    /**
     * @param chunk
     * @return
     */
    private Map<String, Object> checkChunkExits(MultipartFileChunk chunk) {
        Map<String, Object> res = new HashMap<>();
        String identifier = chunk.getIdentifier();
        if (redisTemplate.hasKey(REDIS_KEY_BIG_FILE + identifier)) {
            if (!MinIoUtils.config.isEnabled()) {
                if (!Files.exists(Paths.get(getLocalChunkDir(chunk)))) {
                    redisTemplate.delete(REDIS_KEY_BIG_FILE + identifier);
                }
            }
        }
        if (redisTemplate.hasKey(REDIS_KEY_BIG_FILE + identifier)) {
            HashOperations<Object, Object, Object> hash = redisTemplate.opsForHash();
            Object chunkNumberList = hash.get(REDIS_KEY_BIG_FILE + identifier, "chunkNumberList");
            Object fileUrl = hash.get(REDIS_KEY_BIG_FILE + identifier, "fileUrl");
            res.put("chunkNumbers", chunkNumberList);
            res.put("fileUrl", fileUrl);
        }
        return res;
    }

    private Integer saveChunk(Integer chunkNumber, String identifier) {
        HashOperations<Object, Object, Object> hash = redisTemplate.opsForHash();

        synchronized (this) {
            Set<Integer> oldChunkNumber = (Set<Integer>) hash.get(REDIS_KEY_BIG_FILE + identifier, "chunkNumberList");
            if (Objects.isNull(oldChunkNumber)) {
                Set<Integer> newChunkNumber = new HashSet<>();
                newChunkNumber.add(chunkNumber);
                hash.put(REDIS_KEY_BIG_FILE + identifier, "chunkNumberList", newChunkNumber);
                return newChunkNumber.size();
            } else {
                oldChunkNumber.add(chunkNumber);
                hash.put(REDIS_KEY_BIG_FILE + identifier, "chunkNumberList", oldChunkNumber);
                return oldChunkNumber.size();
            }
        }

    }

    @SneakyThrows
    @PostMapping("/delFile")
    @ResponseBody
    public AjaxResult delFile(@RequestBody MultipartFileChunk chunk) {
        if (StringUtils.isEmpty(chunk.getDirPath())) {
            return AjaxResult.success("文件路径fileDir未传参数,删除失败!", false);
        }
        if (StringUtils.isEmpty(chunk.getFilename())) {
            return AjaxResult.success("文件名filename未传参数,删除失败!", false);
        }
        if (MinIoUtils.config.isEnabled()) {
            String minioFileName = RESOURCE_ROOT + "/" + chunk.getDirPath() + "/" + chunk.getFilename();
            MinIoUtils.deleteFile(minioFileName);
        } else {
            String localFileName = RuoYiConfig.getProfile() + RESOURCE_ROOT + "/" + chunk.getDirPath() + "/" + chunk.getFilename();
            if (StringUtils.isNotEmpty(localFileName)) {
                Path filePath = Paths.get(localFileName.trim());
                if (Files.exists(filePath)) {
                    Files.delete(filePath);
                }
            }
        }
        return AjaxResult.success("文件删除成功", true);
    }
}
